package io.gitee.devaper.starlight.security

import com.google.common.hash.*
import jakarta.servlet.http.*
import org.springframework.data.redis.core.*
import org.springframework.security.core.*
import org.springframework.security.core.userdetails.*
import org.springframework.security.web.authentication.rememberme.*
import java.nio.charset.*
import java.util.*
import java.util.concurrent.*

/**
 * 通过Redis实现的并发会话控制的RememberMeServices
 *
 * 在redis中存储这么一条记录: {prefix}:{username} = {tokenId} , 每次用户通过密码登录应该挤掉其它用户的登录, 此时tokenId变更为新的,
 *      其它用户的tokenId就不是最新的了, 登录时自然也就失效了
 *
 * @param key key
 * @param secret 存到Cookie中签名的密钥(key)信息
 * @param userDetailsService userDetailsService
 * @param redisOperations RedisOperations
 */
open class RedisConcurrentSessionControlRememberMeServices(
    key: String,
    secret: ByteArray,
    userDetailsService: UserDetailsService,
    private val redisOperations: RedisOperations<String, Any>
) : AbstractRememberMeServices(key, userDetailsService) {

    private val hashFunction = Hashing.hmacSha256(secret)

    override fun onLoginSuccess(
        request: HttpServletRequest,
        response: HttpServletResponse,
        successfulAuthentication: Authentication
    ) {
        // 随机生成一个唯一的tokenValue
        val tokenId = generateTokenId()
        // 获取用户名
        val username = retrieveUserName(successfulAuthentication).takeIf { it.isNotEmpty() } ?: return
        // 获取密码, 如果没有从Authentication读取到密码, 那么从userDetailsService重新查一遍
        val password = retrievePassword(successfulAuthentication)?.takeIf { it.isNotEmpty() } ?: let {
            userDetailsService.loadUserByUsername(username).password
        }
        // 创建token签名
        val signature = makeTokenSignature(username, password, tokenId)
        // 将tokenValue存入Redis
        redisOperations.opsForValue().set(
            generateRedisKey(username),
            tokenId,
            tokenValiditySeconds.toLong(),
            TimeUnit.SECONDS
        )
        // 设置Cookie到响应
        setCookie(arrayOf(key, username, tokenId, signature), tokenValiditySeconds, request, response)
    }

    override fun processAutoLoginCookie(
        cookieTokens: Array<out String>,
        request: HttpServletRequest,
        response: HttpServletResponse
    ): UserDetails {
        if (cookieTokens.size != 4) {
            throw InvalidCookieException("Cookie token did not contain 3 or 4 tokens, but contained '${cookieTokens.asList()}'")
        }
        val username = cookieTokens[1]
        val actualTokenId = cookieTokens[2]
        val actualTokenSignature = cookieTokens[3]

        // 检查自己携带的tokenId和服务器携带的是否一致
        val expectedTokenId = redisOperations.opsForValue().get(generateRedisKey(username))?.toString()
        if (expectedTokenId.isNullOrEmpty() || actualTokenId != expectedTokenId) {
            throw InvalidCookieException("Cookie token has expired")
        }
        // 获取userDetails, 拿到密码, tokenSignature是需要密码参与计算的
        val userDetails = userDetailsService.loadUserByUsername(username) ?: throw RememberMeAuthenticationException(
            "UserDetailsService $userDetailsService returned null for username $username. This is an interface contract violation"
        )
        // 检查签名是否一致
        val expectedTokenSignature = makeTokenSignature(userDetails.username, userDetails.password, expectedTokenId)
        if (actualTokenSignature != expectedTokenSignature) {
            throw InvalidCookieException("Cookie contained signature '$actualTokenSignature' but expected '$expectedTokenSignature'")
        }
        return userDetails
    }

    override fun logout(request: HttpServletRequest, response: HttpServletResponse, authentication: Authentication) {
        super.logout(request, response, authentication)

        // if (authentication.isAuthenticated) {
        //     redisOperations.delete(generateRedisKey(retrieveUserName(authentication)))
        // }

    }

    protected open fun retrieveUserName(authentication: Authentication): String {
        val principal = authentication.principal
        if (principal is UserDetails) {
            return principal.username
        }
        return authentication.principal.toString()
    }

    protected open fun retrievePassword(authentication: Authentication): String? {
        val principal = authentication.principal
        if (principal is UserDetails) {
            return principal.password
        }
        return authentication.credentials?.toString()
    }

    /**
     * 生成唯一的tokenId, 用来校验当前有效的rememberMe到底是谁的
     */
    protected fun generateTokenId(): String {
        return UUID.randomUUID().toString()
    }

    protected fun generateRedisKey(username: String): String {
        return "remember:$username"
    }

    private fun makeTokenSignature(username: String, password: String, tokenId: String): String {
        val data = "${username}:${password}:${tokenId}:${key}"
        return hashFunction.hashString(data, StandardCharsets.UTF_8).toString()
    }
}